package com.gragra.parseOptions;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.HashSet;
import java.util.Map;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
/**
 * A simple class that can be used to parse arguments and options from a commandline or other source of String
 * arrays.
 * @author Christoph Teichmann
 * 21.04.2013
 */
public class Parser
{
	/**
	 * this method can be used to obtain the number of arguments that were present in the last given input
	 * @return
	 */
	public int getNumberOfGivenArguments()
	{return this.givenArguments.length;}
	/**
	 * This method can be used to remove a option (referred to by its short name) from this list of
	 * options that must be present. There is no check whether the option was set to necessary or even if the
	 * option exists at all.
	 * @param shortName
	 */
	public void setOptionNotnecessary(String shortName)
	{this.necessary.remove(shortName);}
	/**
	 * the set of all options that must be present in the input
	 */
	private final HashSet<String> necessary = new HashSet<String>();
	/**
	 * the array of arguments that was given the last time the parser was applied to an array of Strings via
	 * the parse method
	 */
	private String[] givenArguments;
	/**
	 * this method can be used to specify that the given option is required, the name given for the
	 * option must be the name that was originally used to create the option, if the option has not
	 * been created yet a CommandLineException will be thrown
	 * @param shortName
	 * @throws CommandLineException 
	 */
	public void setOptionNecessary(String shortName) throws CommandLineException
	{
		if(!this.options.containsKey(shortName))
		{throw new CommandLineException("Can not make undefined option necessary: "+shortName);}
		this.necessary.add(shortName);
	}
	/**
	 * This method can be used to create a new FileWriter directly from the arguments, the number is the
	 * number of the argumetn in the input, counted from left to right, with numbering starting at 0
	 * @param number
	 * @return
	 * @throws IOException 
	 */
	public Writer getFileWriterFromArgument(int number) throws IOException
	{return new PrintWriter(this.givenArguments[number]);}
	/**
	 * this method can be called to directly create a FileReader from part of the arguments, the number is the
	 * number of the argumetn in the input, counted from left to right, with numbering starting at 0
	 * @return
	 * @throws FileNotFoundException 
	 */
	public FileReader getFileReaderFromArgument(int number) throws FileNotFoundException
	{return new FileReader(this.givenArguments[number]);}
	/**
	 * returns the argument at the given position, positions are counted from left to right, counting starts
	 * at 0
	 * @param number
	 * @return
	 */
	public String getArgument(int number)
	{return this.givenArguments[number];}
	/**
	 * the list of arguments that are currently processed
	 */
	private final ObjectArrayList<String> arguments = new ObjectArrayList<String>();
	/**
	 * this method returns the description of the program that has been given to this parser class
	 * @return the programDescription
	 */
	public String getProgramDescription()
	{return this.programDescription;}
	/**
	 * this method can be used to assign a new description to this parser and thereby to the help message
	 * @param programDescription the programDescription to set
	 */
	public void setProgramDescription(String programDescription)
	{this.programDescription = programDescription;}
	/**
	 * the currently used program description
	 */
	private String programDescription = "This is the help message for this program";
	/**
	 * this method is used to ensure that no new option equals "h" or "help"
	 * @param s
	 */
	private void testHelp(String s)
	{
		if(s.equals("h") || s.equals("help"))
		{throw new IllegalArgumentException("It is not allowed to overwrite the help Option");}
	}
	/**
	 * this list contains all the descriptions given for arguments, may be padded with null values
	 */
	private final ObjectArrayList<String> argDesc = new ObjectArrayList<String>();
	/**
	 * teh minimum number of arguments that must be present in the input
	 */
	private int minArgs = 0;
	/**
	 * the maximum number of arguments that are allowed to be present in the input
	 */
	private int maxArgs = Integer.MAX_VALUE;
	/**
	 * the alternative names that have been given for options
	 */
	private final BiMap<String, String> aliases = HashBiMap.create();
	/** 
	 * the options that have been added to this parser
	 */
	private final Object2ObjectOpenHashMap<String, OptionInformation> options = new Object2ObjectOpenHashMap<String, OptionInformation>();
	/**
	 * whether to ignore unknown options in the input
	 */
	private boolean ignoreUnknown = true;
	/**
	 * can be used to check whether unknown options will be ignored
	 * @return the ignoreUnknown
	 */
	public boolean ignoreUnknown()
	{return this.ignoreUnknown;}
	/**
	 * can be used to set whether unknown options will be ignored
	 * @param ignoreUnknown the ignoreUnknown to set
	 */
	public void setIgnoreUnknown(boolean ignoreUnknown)
	{this.ignoreUnknown = ignoreUnknown;}
	/**
	 * can be used to add new options to the list of options the parser looks for
	 */
	public void addOption(String shortName,OptionInformation oi)
	{
		this.testHelp(shortName);
		if(options.containsKey(shortName))
		{throw new IllegalArgumentException("Option with this name already exists: "+shortName);}
		else
		{this.options.put(shortName, oi);}
	}
	/**
	 * can be used to add an alternative name for an option, only one alternative name is allowed per option;
	 * the option the alternative name is to be defined for has to exist
	 * @param shortName
	 * @param additionalName
	 */
	public void addName(String shortName, String additionalName)
	{
		this.testHelp(additionalName);
		if(this.aliases.containsKey(additionalName) || options.containsKey(additionalName))
		{throw new IllegalArgumentException("Option with this name already exists: "+additionalName);}
		if(this.aliases.inverse().containsKey(shortName))
		{throw new IllegalArgumentException("Alias for this Option already exists: "+shortName);}
		if(!this.options.containsKey(shortName))
		{throw new IllegalArgumentException("Option with this name does not exist: "+shortName);}
		this.aliases.put(additionalName, shortName);
	}
	/**
	 * this method is used to parse a commandline. the syntax is: arbitray number of arguments not beginning
	 * with '-',
	 * option indicated by beginning with '-' and then an arbitrary number of arguments, next option beginning
	 * with '-' and then an arbitrary number of arguments, so on
	 * / can be used to mask a leading - in an argument
	 * @param args
	 * @throws CommandLineException if unknown option is present and ignoreUnknown is set to false, if a
	 * necessary option has not been set, if there are not enough or to many arguments, if one of the
	 * OptionInformations throws such an Exception
	 */
	public void parse(String[] args) throws CommandLineException
	{
		findHelp(args);
		this.arguments.clear();
		OptionInformation oi = null;
		for(String string : args)
		{
			if(string.matches("-.*"))
			{oi = processStep(string,oi);}
			else
			{this.arguments.add(string.replaceAll("/-", "-"));}
		}
		processStep(oi);
		for(String s : this.necessary)
		{
			OptionInformation opt = this.options.get(s);
			if(!opt.hasBeenSet())
			{throw new CommandLineException("Option not properly set:"+s);}
		}
	}
	/**
	 * this method is used when a new option is read, it needs to pass all arguments seen to the last option
	 * (or add them to the given arguments if no Option was seen) and then clear the arguments, it also may
	 * check for unknown options
	 * @param string 
	 * @param oi
	 * @return 
	 * @throws CommandLineException 
	 */
	private OptionInformation processStep(String string, OptionInformation oi) throws CommandLineException
	{
		this.processStep(oi);
		string = string.substring(1);
		OptionInformation next = this.options.get(string);
		if(next == null)
		{
			String alias = this.aliases.get(string);
			if(alias != null)
			{next = this.options.get(alias);}
		}
		if(next == null)
		{
			if(!this.ignoreUnknown)
			{throw new CommandLineException("Unknown option present in input: " + string);}
			else
			{
				// we need a dummy option
				next = new OptionInformation()
				{
					@Override
					public void processArguments(ObjectArrayList<String> arguments)
					{}
					@Override
					public String getDescription()
					{return "";}
					@Override
					public boolean hasBeenSet()
					{return false;}
				};
			}
		}
		return next;
	}
	/**
	 * this method passes all the given arguments to the last seen option or sets them as the given
	 * arguments
	 * @param oi
	 * @throws CommandLineException 
	 */
	private void processStep(OptionInformation oi) throws CommandLineException
	{
		if(oi == null)
		{
			if(this.arguments.size() < this.minArgs || this.arguments.size() > maxArgs)
			{throw new CommandLineException("Inadmissible number of Arguments: "
													+ Integer.toString(this.arguments.size()));}
			else
			{this.givenArguments = this.arguments.toArray(new String[this.arguments.size()]);}
		}
		else
		{oi.processArguments(this.arguments);}
		this.arguments.clear();
	}
	/**
	 * this method checks the input for the presence of a help option and if it is present, a help
	 * message will be printed
	 * @param args
	 */
	private void findHelp(String[] args)
	{
		for(String string : args)
		{
			if(string.equals("-help") || string.equals("-h"))
			{
				this.hadHelp = true;
				return;
			}
		}
		this.hadHelp = false;
	}
	/**
	 * will return true if and only if the last input contained a help option of the form '-h' or
	 * '-help'
	 */
	private boolean hadHelp = false;
	/**
	 * can be used to check if the 
	 * @return the hadHelp
	 */
	public boolean hadHelp()
	{return this.hadHelp;}
	/**
	 * this method can be used to print the help message for the current state of this parser
	 */
	public String writeOutHelp()
	{
		StringBuilder sb = new StringBuilder();
		sb.append(this.programDescription);
		sb.append("\n").append("Usage is: argument_1 argument_2 ... -option_1 value_1 value_2 ... -option_2 value_1 value_2 ... ");
		sb.append("\n").append("The minimum Number of arguments is: "+Integer.toString(minArgs));
		sb.append("\n").append("The maximum Number of arguments is: "+Integer.toString(maxArgs));
		sb.append("\n").append("Argument descriptions are:");
		for(int i=0;i<this.argDesc.size();++i)
		{
			String s = this.argDesc.get(i);
			if(s == null)
			{sb.append("\n").append("Argument number: "+Integer.toString(i)+" - no Description given for this Argument");}
			else
			{sb.append("\n").append("Argument number: "+Integer.toString(i)+" - "+s);}
		}
		sb.append("\n").append("The options and their descriptions are:");
		sb.append("\n").append("-h -help print this message");
		for(Map.Entry<String, OptionInformation> ent : this.options.entrySet())
		{
			sb.append("\n");
			String key = ent.getKey();
			String altKey = this.aliases.inverse().get(key);
			sb.append("-"+key+" ");
			if(altKey != null)
			{sb.append("-"+altKey+" ");}
			sb.append(ent.getValue().getDescription());
			if(this.necessary.contains(key))
			{sb.append("\n").append("This option is necessary.");}
		}
		return sb.toString();
	}
	/**
	 * this method can be used to retrieve the OptionInformation associated to a shortName or an
	 * alias, then the OptionInformation can be used to extract information
	 * @return
	 */
	public OptionInformation getInformation(String optionName)
	{
		OptionInformation oi = this.options.get(optionName);
		if(oi == null)
		{
			String alias = this.aliases.get(optionName);
			if(alias  != null)
			{oi = this.options.get(alias);}
		}
		return oi;
	}
	/**
	 * this method can be used to set the minimum number of arguments that must be present in the input
	 * @param minArgs the minArgs to set
	 */
	public void setMinArgs(int minArgs)
	{this.minArgs = minArgs;}
	/**
	 * this methods can be used to set the maximum number of arguments that may be present in the input
	 * @param maxArgs the maxArgs to set
	 */
	public void setMaxArgs(int maxArgs)
	{this.maxArgs = maxArgs;}
	/**
	 * this method can be used to assign a meaning to the given argument, counted from left to right and 
	 * starting with 0
	 */
	public void setArgumentDescription(int argumentNumber, String description)
	{
		if(this.argDesc.size() <= argumentNumber)
		{
			for(;this.argDesc.size()<=argumentNumber;)
			{this.argDesc.add(null);}
		}
		this.argDesc.set(argumentNumber, description);
	}
	
	public void setOption(String shortDescription,ObjectArrayList<String> arguments) throws CommandLineException
	{this.options.get(shortDescription).processArguments(arguments);}
}